package scaladuct.asm

import org.objectweb.asm.{ClassAdapter, ClassWriter, MethodVisitor, Opcodes}

/**
 * <p>
 * Created by IntelliJ IDEA.
 * User: zslajchrt
 * Date: Aug 16, 2010
 * Time: 11:31:15 AM
 */

object Scala2JavaBeanClassWriter {
  val SCALABEAN_FIELD = "$scalaBean"
}

class Scala2JavaBeanClassWriter extends ClassAdapter(new ClassWriter(0)) {
  var originalNameWithSlashes: String = null
  var classnameWithSlashes: String = null
  var classnameWithDots: String = null
  var scalaBeanFieldDesc: String = null

  val writer = cv.asInstanceOf[ClassWriter];

  override def visit(version: Int, access: Int, name: String, signature: String, superName: String, interfaces: Array[String]) = {
    originalNameWithSlashes = name
    classnameWithSlashes = name + "Bean"
    classnameWithDots = classnameWithSlashes.replace('/', '.')
    scalaBeanFieldDesc = "L" + name + ";"
    //super.visit(Opcodes.V1_5, access, classnameWithSlashes, signature, superName, interfaces)
    super.visit(Opcodes.V1_5, access, classnameWithSlashes, signature, "java/lang/Object", Array())

    val fv = cv.visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_FINAL, Scala2JavaBeanClassWriter.SCALABEAN_FIELD, scalaBeanFieldDesc, null, null);
    fv.visitEnd();

  }

  override def visitField(access: Int, name: String, desc: String, signature: String, value: Any) = {
    null
  }

  override def visitMethod(access: Int, name: String, desc: String, signature: String, exceptions: Array[String]) = {
    def capitalize(n: String) = n.charAt(0).toUpper + n.substring(1)

    // generate JavaBean style wrappers
    if (name.startsWith("<init>") || name.startsWith("<clinit>"))
      null
    else if (desc.startsWith("()") && !name.startsWith("get")) {
      val newName = "get" + capitalize(name)
      generateGetter(super.visitMethod(access, newName, desc, signature, exceptions), name, desc)
      null
    } else if (name.endsWith("_$eq")) {
      val newName = "set" + capitalize(name.substring(0, name.length - "_$eq".length))
      generateSetter(super.visitMethod(access, newName, desc, signature, exceptions), name, desc)
      null
    } else
    //super.visitMethod(access, name, desc, signature, exceptions)
      null
  }

  override def visitEnd = {
    generateConstructor
  }

  private def generateConstructor {
    val visitConstr: MethodVisitor = super.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "(L" + originalNameWithSlashes + ";)V", null, null)
    visitConstr.visitIntInsn(Opcodes.ALOAD, 0);
    visitConstr.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V")

    visitConstr.visitIntInsn(Opcodes.ALOAD, 0);
    //Stack: [this]
    visitConstr.visitIntInsn(Opcodes.ALOAD, 1);
    visitConstr.visitTypeInsn(Opcodes.CHECKCAST, originalNameWithSlashes)
    //Stack: [this, this.$scalaBean]
    visitConstr.visitFieldInsn(Opcodes.PUTFIELD, classnameWithSlashes, Scala2JavaBeanClassWriter.SCALABEAN_FIELD, scalaBeanFieldDesc);


    visitConstr.visitInsn(Opcodes.RETURN)
    visitConstr.visitMaxs(3, 2)
    visitConstr.visitEnd()
  }


  private def generateSetter(setterVisitor: MethodVisitor, name: String, desc: String) {
    setterVisitor.visitCode()

    var maxStack = 2;
    var maxLocals = 2;

    setterVisitor.visitIntInsn(Opcodes.ALOAD, 0);
    //Stack: [this]

    setterVisitor.visitFieldInsn(Opcodes.GETFIELD, classnameWithSlashes, Scala2JavaBeanClassWriter.SCALABEAN_FIELD, scalaBeanFieldDesc);
    //Stack: [this.$scalaBean]

    val RefDesc = """\(L(.*);\)V""".r
    val ScalarDesc = """\(([BSIJFDZC])\)V""".r

    desc match {
      case RefDesc(refDesc) => {
        setterVisitor.visitIntInsn(Opcodes.ALOAD, 1)
        //Stack: [this.$scalaBean, arg0]

        setterVisitor.visitTypeInsn(Opcodes.CHECKCAST, refDesc)
        //Stack: [this.$scalaBean, arg0]

        maxStack += 1

      }
      case ScalarDesc(scalarDesc) =>
        scalarDesc match {
          case "B" =>
            setterVisitor.visitIntInsn(Opcodes.ILOAD, 1)
          case "S" =>
            setterVisitor.visitIntInsn(Opcodes.ILOAD, 1)
          case "I" =>
            setterVisitor.visitIntInsn(Opcodes.ILOAD, 1)
          case "J" => {
            setterVisitor.visitIntInsn(Opcodes.LLOAD, 1)
            maxStack += 1
            maxLocals += 1
          }
          case "F" =>
            setterVisitor.visitIntInsn(Opcodes.FLOAD, 1)
          case "D" => {
            setterVisitor.visitIntInsn(Opcodes.DLOAD, 1)
            maxStack += 1
            maxLocals += 1
          }
          case "Z" =>
            setterVisitor.visitIntInsn(Opcodes.ILOAD, 1)
          case "C" =>
            setterVisitor.visitIntInsn(Opcodes.ILOAD, 1)
        }
      case _ => throw new UnsupportedOperationException(desc)
    }

    setterVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, originalNameWithSlashes, name, desc)

    setterVisitor.visitInsn(Opcodes.RETURN)

    setterVisitor.visitMaxs(maxStack, maxLocals)
    setterVisitor.visitEnd()

  }

  private def generateGetter(getterVisitor: MethodVisitor, name: String, desc: String) {
    getterVisitor.visitCode()

    var maxStack = 2;
    var maxLocals = 1;

    getterVisitor.visitIntInsn(Opcodes.ALOAD, 0);
    //Stack: [this]

    getterVisitor.visitFieldInsn(Opcodes.GETFIELD, classnameWithSlashes, Scala2JavaBeanClassWriter.SCALABEAN_FIELD, scalaBeanFieldDesc);
    //Stack: [this.$scalaBean]

    getterVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, originalNameWithSlashes, name, desc)
    //Stack: [result]

    val RefDesc = """\(\)L(.*);""".r
    val ScalarDesc = """\(\)([BSIJFDZC])""".r

    desc match {
      case RefDesc(refDesc) => {
        getterVisitor.visitInsn(Opcodes.ARETURN)
      }
      case ScalarDesc(scalarDesc) =>
        scalarDesc match {
          case "B" =>
            getterVisitor.visitInsn(Opcodes.IRETURN)
          case "S" =>
            getterVisitor.visitInsn(Opcodes.IRETURN)
          case "I" =>
            getterVisitor.visitInsn(Opcodes.IRETURN)
          case "J" => {
            getterVisitor.visitInsn(Opcodes.LRETURN)
            maxStack += 1
          }
          case "F" =>
            getterVisitor.visitInsn(Opcodes.FRETURN)
          case "D" => {
            getterVisitor.visitInsn(Opcodes.DRETURN)
            maxStack += 1
          }
          case "Z" =>
            getterVisitor.visitInsn(Opcodes.IRETURN)
          case "C" =>
            getterVisitor.visitInsn(Opcodes.IRETURN)
        }
      case _ => throw new UnsupportedOperationException(desc)
    }

    getterVisitor.visitMaxs(maxStack, maxLocals)
    getterVisitor.visitEnd()

  }

  class Scala2JavaBeanClassLoader() extends ClassLoader {
    def loadCookieClass(): Class[_] =
      loadClass(classnameWithDots)

    override def findClass(name: String) = {
      if (classnameWithDots == name) {
        val byteCode: Array[Byte] = writer.toByteArray
        defineClass(name, byteCode, 0, byteCode.length)
      } else
        null
    }
  }
}
